package org.west.sky.scripture.imports.core.base;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.exception.ExcelCommonException;
import org.apache.poi.ss.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.west.sky.scripture.imports.core.lock.Lock;
import org.west.sky.scripture.imports.core.lock.LockAspect;
import org.west.sky.scripture.imports.core.observer.ImportObserver;
import org.west.sky.scripture.imports.core.observer.TaskCenterRecordObserver;
import org.west.sky.scripture.imports.core.observer.WebsocketObserver;

import javax.servlet.http.HttpServletRequest;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * 批量导入版本2
 *
 * @param <T> excel读取实体对象
 * @param <E> 自定义参数
 */

public interface ImportController<T extends ImportData, E> {

    Logger logger = LoggerFactory.getLogger(ImportController.class);

    int EXCEL_MAX_DATA_SIZE = 10000;

    ExecutorService executor = Executors.newFixedThreadPool(10);

    default PlatformTransactionManager platformTransactionManager() {
        return SpringBeanUtil.getBean(PlatformTransactionManager.class);
    }

    ImportService<T, E> importService();

    /**
     * excel文件校验，只校验文件格式、条数
     *
     * @param file
     * @param request
     * @return
     * @throws IOException
     */
    @PostMapping(value = "checkExcelData")
    default Result<String> checkExcel(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws IOException {
        E params = getParams(request);
        //系统级别的校验，如采暖季是否存在
        validateBasic(params);
        String fileName = file.getOriginalFilename();
        if (!validateExcel(fileName)) {
            return Result.error(402, "仅支持导入xls、xlsx类型的文件");
        }
        try {
            EasyExcel.read(file.getInputStream(), getImportEntityClass(), new ImportDataSyncListener<>(this::checkDataSize, getValidateColumns(params), getImportEntityClass())).sheet().doRead();
        } catch (BusinessException e) {
            return Result.error(502, e.getMessage());
        } catch (Exception e) {
            if (e instanceof ExcelCommonException) {
                return Result.error(502, "文件可能无效，请检查文件");
            }
            return Result.error(502, "解析失败");
        }
        return Result.ok();
    }

    /**
     * 上传需要导入的文件
     *
     * @param file
     * @param request
     * @return
     * @throws IOException
     */
    @Lock(message = "目前系统存在同类型导入任务尚未完成，请稍后再试", async = true)
    @PostMapping(value = "uploadExcelData")
    default Result<String> upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws IOException {
        E params = getParams(request);
        //系统级别的校验，如采暖季是否存在
        validateBasic(params);
        String fileName = file.getOriginalFilename();
        if (!validateExcel(fileName)) {
            return Result.error(402, "仅支持导入xls、xlsx类型的文件");
        }
        Map<String, Object> taskInfo = importService().getTaskInfo();
        String errorFileName = taskInfo.get("errorFileName").toString() + fileName.substring(fileName.lastIndexOf("."));
        //记录任务
        String taskId = UUID.randomUUID().toString().replaceAll("-", "");
        // 开始复制文件
        String oriFileName = DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + taskInfo.get("errorFileName") + "原始文件" + fileName.substring(fileName.lastIndexOf("."));
        String oriFilePath = uploadOriFile(oriFileName, file.getBytes());
        taskInfo.put("oriFilePath", oriFilePath);
        try {
            EasyExcel.read(file.getInputStream(), getImportEntityClass(), new ImportDataSyncListener<>(data -> this.processData(data, taskId, taskInfo, oriFilePath, errorFileName, params), getValidateColumns(params), getImportEntityClass())).sheet().doRead();
        } catch (Exception e) {
            logger.error("导入失败，{}", e.getMessage(), e);
            if (e instanceof ExcelCommonException) {
                return Result.error(502, "文件可能无效，请检查文件");
            }
            if (e instanceof BusinessException) {
                return Result.error(502, e.getMessage());
            }
            return Result.error(502, "导入失败");
        }
        return Result.ok(taskId);
    }

    default String uploadOriFile(String fileName, byte[] bytes) {
        FileUtil.writeBytes(bytes, "E:\\uploadFile\\原始文件\\" + fileName);
        return "E:\\uploadFile\\原始文件\\" + fileName;
    }

    /**
     * 获取导入对象
     *
     * @return
     */
    default Class<T> getImportEntityClass() {
        Type type = this.getClass().getGenericInterfaces()[0];
        ParameterizedType pt = (ParameterizedType) type;
        return (Class<T>) pt.getActualTypeArguments()[0];
    }

    /**
     * 验证EXCEL文件
     *
     * @param filePath
     * @return
     */
    default boolean validateExcel(String filePath) {
        return ImportValidator.validateExcel(filePath);
    }

    /**
     * 校验数据条数
     *
     * @param data
     */
    default void checkDataSize(List<T> data) {
        //校验数据条数
        int dataSize = getExcelMaxDataSize();
        if (CollectionUtils.isEmpty(data)) {
            throw new BusinessException("未读取到有效数据，请检查文件是否为空文件");
        }
        if (data.size() > dataSize) {
            throw new BusinessException(StrUtil.format("仅支持导入{}条数据以内的文件", dataSize));
        }
        if (importService().verifyDuplicate()) {
            StringBuilder msg = new StringBuilder();
            Map<T, Integer> keyMap = new HashMap<>();
            for (int i = 0; i < data.size(); i++) {
                T t = data.get(i);
                Integer rowNum;
                if ((rowNum = keyMap.get(t)) != null) {
                    msg.append("表格第").append(i + 2).append("行数据与第").append(rowNum).append("行重复;");
                } else {
                    keyMap.put(t, i + 2);
                }
            }
            if (msg.length() != 0) {
                throw new BusinessException(StrUtil.format("存在重复数据:{}", msg.toString()));
            }
        }
    }

    /**
     * 处理业务，业务校验、存储
     *
     * @param data
     */
    default void processData(List<T> data, String taskId, Map<String, Object> taskInfo, String originalFilePath, String fileName, E params) {
        //校验数据条数
        int dataSize = getExcelMaxDataSize();
        if (CollectionUtils.isEmpty(data)) {
            throw new BusinessException("未读取到有效数据，请检查文件是否为空文件");
        }
        if (data.size() > dataSize) {
            throw new BusinessException(StrUtil.format("仅支持导入{}条数据以内的文件", dataSize));
        }
        ImportService<T, E> service = importService();
        //todo 数据去重
        executor.submit(() -> {
            //
            AtomicInteger successNum = new AtomicInteger();
            AtomicInteger failNum = new AtomicInteger();
            try {
                notice(taskId, taskInfo, ImportDataExecuteResult.Progress.ANALYSIS_ENDED, data.size(), successNum.get(), failNum.get(), null);
                PlatformTransactionManager platformTransactionManager = platformTransactionManager();
                List<T> ts = data.stream().filter(ImportData::isCorrect).collect(Collectors.toList());
                if (!CollectionUtils.isEmpty(ts)) {
                    service.checkData(ts, params);
                }
                //从excel中成功解析的数据
                List<T> excelLegalList = ts.stream().filter(T::isCorrect).collect(Collectors.toList());
                if (CollectionUtils.isEmpty(excelLegalList)) {
                    String filePath = uploadHandledData(data, originalFilePath, fileName);
                    notice(taskId, taskInfo, ImportDataExecuteResult.Progress.IMPORT_ENDED, data.size(), 0, data.size(), filePath);
                    return;
                }
                failNum.addAndGet(data.size() - excelLegalList.size());
                notice(taskId, taskInfo, ImportDataExecuteResult.Progress.IMPORTING, data.size(), 0, failNum.get(), null);
                for (List<T> l : importService().partition(excelLegalList)) {
                    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
                    definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
                    TransactionStatus transaction = platformTransactionManager.getTransaction(definition);
                    try {
                        service.saveData(l, params);
                        platformTransactionManager().commit(transaction);
                    } catch (Exception e) {
                        //回滚数据改为逐条执行
                        platformTransactionManager.rollback(transaction);
                        logger.error("执行批量保存操作报错，{}", e.getMessage(), e);
                        l.forEach(t -> {
                            DefaultTransactionDefinition definition_single = new DefaultTransactionDefinition();
                            definition_single.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
                            TransactionStatus transaction_single = platformTransactionManager.getTransaction(definition_single);
                            try {
                                service.saveData(Collections.singletonList(t), params);
                                platformTransactionManager.commit(transaction_single);
                            } catch (Exception ex) {
                                platformTransactionManager.rollback(transaction_single);
                                logger.error("执行单条保存操作报错，{}", ex.getMessage(), ex);
                                t.setCorrect(false);
                                String errorMsg = "请联系管理员";
                                if (ex instanceof BusinessException) {
                                    errorMsg = ex.getMessage();
                                }
                                t.setErrorMsg("导入错误，" + errorMsg);
                            }
                        });
                    }
                    //保存成功的数据
                    List<T> saveSuccessList = l.stream().filter(T::isCorrect).collect(Collectors.toList());
                    successNum.addAndGet(saveSuccessList.size());
                    failNum.addAndGet(l.size() - saveSuccessList.size());
                    notice(taskId, taskInfo, ImportDataExecuteResult.Progress.IMPORTING, data.size(), successNum.get(), failNum.get(), null);
                }
                String filePath = uploadHandledData(data, originalFilePath, fileName);
                notice(taskId, taskInfo, ImportDataExecuteResult.Progress.IMPORT_ENDED, data.size(), successNum.get(), failNum.get(), filePath);
            } catch (Exception e) {
                logger.error("数据业务处理失败", e);
                //出现异常也需要告知前端当前进度
                //出现异常时未处理的数据应该标注
                int handledSize = successNum.get() + failNum.get();
                List<T> notHandledList = data.subList(handledSize, data.size());
                notHandledList.forEach(l -> {
                    l.setCorrect(false);
                    l.setErrorMsg("异常中断，请重新导入");
                });
                String filePath = null;
                try {
                    filePath = uploadHandledData(data, originalFilePath, fileName);
                } catch (Exception ignored) {
                }
                //todo 出现了不可控异常其实也算中断的一种
                notice(taskId, taskInfo, ImportDataExecuteResult.Progress.IMPORT_ENDED, data.size(), successNum.get(), data.size() - successNum.get(), filePath);
            }
        });
    }


    /**
     * 上传已处理的数据，方便用户下载核对失败数据
     *
     * @param data
     */
    default String uploadHandledData(List<T> data, String originalFilePath, String fileName) {
        String filePath = "";
        // 只需要在原来用户数据的基础上导出错误原因即可
        try (FileInputStream fis = new FileInputStream(originalFilePath);
             Workbook workbook = WorkbookFactory.create(fis);
             FileOutputStream fos = new FileOutputStream("E:\\uploadFile\\错误文件\\" + fileName)) {
            Sheet sheet = workbook.getSheetAt(0);
            int failReasonColum = maxEmptyColumIndex(sheet);
            sheet.setColumnWidth(failReasonColum, 50 * 256);
            sheet.setColumnHidden(failReasonColum, false);
            Cell failReasonCell = sheet.getRow(0).createCell(failReasonColum);
            failReasonCell.setCellValue("错误原因");
            //红色背景，根据需求改动
            CellStyle cellStyle = workbook.createCellStyle();
            cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
            cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            failReasonCell.setCellStyle(cellStyle);
            int row = 1;
            for (T t : data) {
                if (!t.isCorrect()) {
                    sheet.getRow(row).createCell(failReasonColum).setCellValue(t.getErrorMsg());
                }
                row++;
            }
            workbook.write(fos);
        } catch (Exception ex) {
            logger.error("错误原因写入失败", ex);
        }
        return "E:\\uploadFile\\错误文件\\" + fileName;
    }

    /**
     * FIXME 暂不考虑多表头的影响
     *
     * @param sheet
     * @return
     */
    default int maxEmptyColumIndex(Sheet sheet) {
        Row headRow = sheet.getRow(0);
        short lastCellNum = headRow.getLastCellNum();
//        Cell lastCell = headRow.getCell(lastCellNum - 1);
//        if(lastCell.getStringCellValue().equals("错误原因")){
//            return lastCellNum - 1;
//        }
        return lastCellNum;
    }

    /**
     * 获取excel最大数据行数
     *
     * @return
     */
    default int getExcelMaxDataSize() {
        return EXCEL_MAX_DATA_SIZE;
    }


    /**
     * 系统级别校验，如供暖季是否存在等
     */
    default void validateBasic(E params) {
    }

    default List<ImportObserver> registerObservers() {
        List<ImportObserver> observers = new ArrayList<>();
        observers.add(SpringBeanUtil.getBean(TaskCenterRecordObserver.class));
        observers.add(SpringBeanUtil.getBean(WebsocketObserver.class));
        observers.add(SpringBeanUtil.getBean(LockAspect.class));
        return observers;
    }

    /**
     * 导入任务执行进度通知
     *
     * @param taskId
     * @param taskInfo
     * @param progress
     * @param totalNum
     * @param successNum
     * @param failNum
     * @param fileUrl
     */
    default void notice(String taskId, Map<String, Object> taskInfo, ImportDataExecuteResult.Progress progress, int totalNum, int successNum, int failNum, String fileUrl) {
        List<ImportObserver> observers = registerObservers();
        int executeStatus = progress == ImportDataExecuteResult.Progress.IMPORT_ENDED ? 1 : 0;
        ImportDataExecuteResult result = new ImportDataExecuteResult(executeStatus, progress, totalNum, successNum, failNum, fileUrl);
        observers.forEach(l -> l.update(taskId, taskInfo, result));
    }

    /**
     * 获取自定义传递参数
     *
     * @param request
     * @return
     */
    default E getParams(HttpServletRequest request) {
        return null;
    }

    /**
     * 获取需要校验的参数
     *
     * @param params
     * @return
     */
    default List<String> getValidateColumns(E params) {
        return null;
    }
}
